package org.swift.mashup.engine.deployment;

import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;
import org.jdom.JDOMException;
import org.swift.commons.xml.XmlUtils;
import org.swift.commons.util.StringUtils;
import org.swift.commons.util.io.FileUtils;
import org.swift.mashup.engine.util.XPathUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamException;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import java.io.*;
import java.util.*;

public class MashupImportPreprocessor implements MashupPreprocessor {
	protected static final String EXT_MASHUP = ".mux";
	protected static final String EXT_MASHUP_REGEX = "\\.mux$";

	protected static final String NAME_IMPORT = "import";
	protected static final String ATTR_NAME = "name";
	protected static final String ATTR_IMPORT_FILE = "file";
	protected static final String ELEM_PARAMETER = "parameter";
	protected static final String NS_MASHUP = XPathUtil.PREFIX;
	protected static final String NS_SCHEMA = XPathUtil.SCHEMA_PREFIX + ":schema";
	protected static final String XPATH_MASHUP = "/"+NS_MASHUP+":mashup";
	protected static final String XPATH_WITH = XPATH_MASHUP+"/"+NS_MASHUP+":with";
	protected static final String XPATH_GRAB = XPATH_MASHUP+"/"+NS_MASHUP+":grab";
	protected static final String XPATH_PUBLISH = XPATH_MASHUP+"/"+NS_MASHUP+":publish";
	protected static final String XPATH_IMPORT = XPATH_MASHUP+"/"+NS_MASHUP+":"+NAME_IMPORT;
	protected static final String XPATH_SCHEMA = XPATH_MASHUP+"/"+ NS_SCHEMA;

	private static Logger log = Logger.getLogger(MashupDeployer.class);

	private String mashupDirectory;
	private String rootMashupDirectory;
	private Map<String,String> importNamespaces = new HashMap<String,String>();
	private final MashupDependentsMap mashupDependentsMap;
	private Map<String,String> mashupLocations;

	public MashupImportPreprocessor(String mashupDirectory, String rootMashupDirectory, MashupDependentsMap mashupDependentsMap, Map<String,String> mashupLocations) {
		this.mashupDependentsMap = mashupDependentsMap;
		this.mashupDirectory = mashupDirectory;
        this.rootMashupDirectory = rootMashupDirectory;
		this.mashupLocations = mashupLocations;

		if ( !this.mashupDirectory.endsWith(File.separator) ) {
			this.mashupDirectory += File.separator;
		}
	}

	@Override
	public InputStream processMashup(InputStream original, String name)
			throws XMLStreamException, IOException {

		Document doc = getDocument(original);
		List<String> imports = getImportMuxs(doc);
		doc = importMuxsIntoDocument(name, imports, doc);

		final File newMashupFile = getNewFile(name);
		writeResult(doc, newMashupFile);

		return new FileInputStream(newMashupFile);
	}

	protected File getMashupFile(String filename) throws IOException {
		String name = filename;
		if ( !name.endsWith(EXT_MASHUP) ) {
			name = name + EXT_MASHUP;
		}
		File file = null;
		String location = this.mashupLocations.get(name);
		if(StringUtils.isNotBlank(location)){
			file = new File(location);
		}else{
			file = FileUtils.findFile(name, new File(this.rootMashupDirectory));
			if(file==null){
				throw new IOException("Unable to locate file for mashup "+filename);
			}
            this.mashupLocations.put(name, file.getAbsolutePath());
		}

		return file;
	}

	protected Document getDocument(File mashupFile) throws IOException {
		Document doc = null;
		InputStream stream = new FileInputStream(mashupFile);

		try {
			doc = getDocument(stream);
		}
		finally {
			stream.close();
		}

		return doc;
	}

	protected Document getDocument(InputStream original) throws IOException {
		try {
			return XmlUtils.toDocument(IOUtils.toString(original));
		}
		catch (ParserConfigurationException e) {
			throw new RuntimeException(e.getMessage(), e);
		}
		catch (SAXException e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}

	protected List<String> getImportMuxs(Document doc) {
		List<String> files = new ArrayList<String>();

		try {
			XPathExpression expr = XPathUtil.createXPathExpression(XPATH_IMPORT + "/@" + ATTR_IMPORT_FILE, XPathUtil.getContext());
			NodeList imports = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);

			for (int i = 0; i < imports.getLength(); i++) {
				files.add( imports.item(i).getNodeValue() );
			}
		}
		catch (XPathExpressionException e) {
			throw new RuntimeException(e.getMessage(), e);
		}

		return files;
	}

	protected Map<String, String> getImportParameters(String importFile, Document doc) {
		Map<String, String> paramMap = new HashMap<String, String>();

		try {
			final String exprString = XPATH_IMPORT + "[@" + ATTR_IMPORT_FILE + "='" + importFile + "']/"
				+ NS_MASHUP+ ":" + ELEM_PARAMETER;
			XPathExpression expr = XPathUtil.createXPathExpression(exprString, XPathUtil.getContext());
			NodeList params = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);


			for (int i = 0; i < params.getLength(); i++) {
				Node param = params.item(i);
				NamedNodeMap attrs = param.getAttributes();
				String name = attrs.getNamedItem("name").getNodeValue();
				String value = attrs.getNamedItem("value").getNodeValue();
				paramMap.put(name, value);
			}
		}
		catch (XPathExpressionException e) {
			throw new RuntimeException(e.getMessage(), e);
		}

		return paramMap;
	}

	protected Document importMuxsIntoDocument(String importer, List<String> listImportMuxs, Document doc)
			throws IOException {
		Map<String,Node> nodeMap = new HashMap<String, Node>();

		for ( String importMux : listImportMuxs) {
			Map<String, String> params = getImportParameters(importMux, doc);
			nodeMap.putAll( getImportedNodes(importMux, new Stack<String>(), params, importer) );
		}

		for ( Map.Entry<String,Node> entry : nodeMap.entrySet() ) {
			String nodeName = entry.getKey();
			Node node = entry.getValue();

			if ( !docHasNode(doc, nodeName) ) {
				doc.getFirstChild().appendChild( doc.importNode(node, true) );
			}
		}
		
		//convert doc to jdom and then back to 
		org.jdom.input.DOMBuilder db = new org.jdom.input.DOMBuilder();
		org.jdom.Document jdoc = db.build(doc);
		org.jdom.Element jroot = jdoc.getRootElement();
		for(Map.Entry<String,String> entry : importNamespaces.entrySet()){
			jroot.addNamespaceDeclaration(org.jdom.Namespace.getNamespace(entry.getKey(), entry.getValue()));
		}
		org.jdom.output.DOMOutputter dout = new org.jdom.output.DOMOutputter();
		try {
			doc = dout.output(jdoc);
		} catch (JDOMException e) {
			log.warn("Error converting from jdom to w3c document. "+e);
		}
		return doc;
	}

	protected void recordDependency(String importedMuxName, File importedFile, String importer) {
		mashupDependentsMap.addDependent(importedMuxName, importedFile.lastModified(), importer);
	}

	protected Map<String,Node> getImportedNodes(String importMux, Stack<String> importStack, 
			Map<String, String> params, String importer) throws IOException {

		importMux = trimFileExtension(importMux);
		File importedFile = getMashupFile(importMux);

		checkForCircularDependency(importMux, importStack);
		recordDependency(importMux, importedFile, importer);

		Document importedDoc = getDocument(importedFile);
		importStack.push(importMux);

		// nested imports are done first so that when nodes from the current mashup are put into the map,
		//  they will override anything with the same name
		Map<String,Node> importedNodes = handleNestedImports(importer, importedDoc, importStack);
		
		// convert w3c doc to jdom to access namespaces since w3c doesn't provide any easy way of doing this
		org.jdom.input.DOMBuilder db = new org.jdom.input.DOMBuilder();
		org.jdom.Document jdoc = db.build(importedDoc);
		org.jdom.Element jroot = jdoc.getRootElement();
		List<org.jdom.Namespace> namespaces = jroot.getAdditionalNamespaces();
		for(org.jdom.Namespace namespace: namespaces){
			importNamespaces.put(namespace.getPrefix(), namespace.getURI());
		}	

		NodeList nodes = getNodeList(importedDoc, XPATH_WITH + "|" + XPATH_GRAB + "|" + XPATH_PUBLISH);
		for ( int i = 0; i < nodes.getLength(); i++ ) {
			Node node = nodes.item(i);
			String nodeName = node.getNodeName();
			String name = node.getAttributes().getNamedItem(ATTR_NAME).getTextContent();
			if((NS_MASHUP+ ":with").equalsIgnoreCase(nodeName) && params.size() > 0) {
				Node clonedWith = node.cloneNode(true);
				NamedNodeMap attrs = clonedWith.getAttributes();
				Node withName = attrs.getNamedItem("name");
				Node value = attrs.getNamedItem("value");
				if(value == null) {
					((Element)clonedWith).setAttribute("value", params.get(name));
				}
				else  {
					value.setNodeValue(params.get(name));
				}
				node = clonedWith;
			}
			importedNodes.put(name, node);
		}
		nodes = getNodeList(importedDoc, XPATH_SCHEMA);
		for ( int i = 0; i < nodes.getLength(); i++ ) {
			Node node = nodes.item(i);
			String name = node.getNodeName();
			importedNodes.put(name, node);
		}

		importStack.pop();
		return importedNodes;
	}

	protected void checkForCircularDependency(String importMux, Stack<String> importStack) {
		final int NOT_IN_STACK = -1;
		if ( importStack.search(importMux) != NOT_IN_STACK ) {
			final String delim = " -> ";
			final String msg = "Circular dependency: " + stackToString(importStack,delim) + delim + importMux;
			log.warn(msg);
			throw new RuntimeException(msg);
		}
	}

	protected Map<String, Node> handleNestedImports(String importer, Document doc, Stack<String> importStack)
			throws IOException {

		Map<String,Node> nodeMap = new HashMap<String,Node>();
		NodeList nodes = getNodeList(doc, XPATH_IMPORT);

		for ( int i = 0; i < nodes.getLength(); i++ ) {
			Node node = nodes.item(i);
			String nextImport = node.getAttributes().getNamedItem(ATTR_IMPORT_FILE).getTextContent();
			Map<String, String> params = getImportParameters(nextImport, doc);
			nodeMap.putAll(getImportedNodes(nextImport, importStack, params, importer));
		}

		return nodeMap;
	}

	protected NodeList getNodeList(Document doc, String xpath) {
		try {
			XPathExpression expr = XPathUtil.createXPathExpression(xpath, XPathUtil.getContext());
			return (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
		}
		catch (XPathExpressionException e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}

	protected boolean docHasNode(Document doc, String name) {
		if(NS_SCHEMA.equals(name)){
			return docHasSchemaNode(doc, name);
		}
		return docHasNamedNode(doc, name);
	}
	
	protected boolean docHasNamedNode(Document doc, String name) {
		final String xpath = String.format("%s/node()[@%s='%s']", XPATH_MASHUP, ATTR_NAME, name);
		NodeList nodes = getNodeList(doc, xpath);
		return nodes != null && nodes.getLength() > 0;
	}

	protected boolean docHasSchemaNode(Document doc, String name) {
		final String xpath = String.format("%s/node()[name()='%s']", XPATH_MASHUP,NS_SCHEMA);
		NodeList nodes = getNodeList(doc, xpath);
		return nodes != null && nodes.getLength() > 0;
	}

	protected String stackToString(Stack<String> stack, String delimiter) {
		StringBuilder sb = new StringBuilder();

		Iterator<String> i = stack.iterator();
		while (i.hasNext()) {
			sb.append( i.next() );
			if ( i.hasNext() ) {
				sb.append(delimiter);
			}
		}

		return sb.toString();
	}

	protected File getNewFile(String name) throws IOException {
		final File newMashupFile = File.createTempFile(trimFileExtension(name), EXT_MASHUP);
		newMashupFile.deleteOnExit();
		return newMashupFile;
	}

	protected String trimFileExtension(String name) {
		return name.replaceAll(EXT_MASHUP_REGEX,"");
	}

	protected void writeResult(Document doc, File newMashupFile) throws IOException {
		OutputFormat format = new OutputFormat(doc);
		Writer writer = new BufferedWriter(new FileWriter(newMashupFile));
		XMLSerializer serial = new XMLSerializer(writer, format);
		serial.serialize(doc);
		writer.flush();
		writer.close();
	}
}
